3.3 継承の裏側
C++のクラスは継承を行うことができます.継承によって派生クラスを定義すると,Cの構造体とはかなり掛け離れた存在になってしまいます.ここでは,継承を行った場合のメモリレイアウトと継承関係にあるクラスへのポインタを型変換した場合について解説することにします.
3.3.1 継承を行った場合のメモリレイアウト
まずは簡単な継承の例を取り上げることにしましょう.
struct A
{
char a;
};
struct B : A
{
short b;
};
上記は非常に単純な継承の例です.記述を簡単にするためにstructを用いて構造体にしていますが,classを使った場合でも本質的な違いはありません.継承を行った場合のメモリレイアウトについては,言語規格で明確に規定されているわけではありませんが,ほとんどの場合,図3.2のようになると考えてよいでしょう.
●図3.2 継承を行った場合のメモリレイアウト
すなわち,これを擬似コードで表せば,次の構造体と同じであると考えることができます.
struct B
{
A __A;
short b;
};
ただし,実際には,次のようにレイアウトされる可能性もあります.
struct B
{
short b;
A __A;
};
多重継承の場合も考えてみましょう.
struct A
{
char a;
};
struct B
{
short b;
};
struct C : A, B
{
int c;
};
上記のコードの構造体Cを擬似コードで表せば,たとえば,次のようになります.
struct C
{
A __A;
B __B;
int c;
};
3.3.2 継承関係にあるクラスへのポインタを 型変換したとき
ここで注意しなければならないのは,前項で示した次のコードのレイアウトや多重継承時のコードのレイアウトの場合,ポインタの型変換を行うと,ポインタの値が変化する可能性があるということです.
struct B
{
short b;
A __A;
};
struct C
{
A __A;
B __B;
int c;
};
多重継承のC構造体を例として,詳しく見ていくことにしましょう.
C object;
C* pc = &object;
A* pa = pc;
B* pb = pb;
派生クラスへのポインタは,上記のように,基底クラスへのポインタ型に暗黙的に型変換することができます.このとき,A*に型変換した場合には,pcの値とpaの値は同じになります.しかし,B*に型変換した場合には,pcの値とpbの値は明らかに異なります(図3.3参照).
●図3.3 基底クラスへのポインタの参照先
実際には,次のようなレイアウトになるかもしれません.
struct C
{
int c;
A __A;
B __B;
};
また,次の「3.4 仮想関数の裏側」で解説しますが,クラスに仮想関数が含まれていると,さらにレイアウトが変化するため,派生クラスへのポインタから基底クラスへのポインタ型に型変換した場合の値は変わると考えたほうがよさそうです.
Cでは,どんなに無茶なポインタどうしの型変換を行っても,値が変わることはありませんでした.しかし,C++では,このように型変換によってポインタの値が変化することが普通に起こるのです.